//===============================================================================
// Microsoft patterns & practices Enterprise Library Contribution
// Common Core Extensions
//===============================================================================

using System;
using System.Data;
using System.Globalization;
using EntLibContrib.Common.Properties;

namespace EntLibContrib.Common
{
	/// <summary>
	/// This class allows a <see cref="DataRow"/> to be written to throuigh a common interface.
	/// Since it implements <see cref="INullableWriter{T}"/>, it provides methods to write both
	/// non-null and nullable data fields.
	/// </summary>
	public class NullableDataRowWriter : INullableWriter<DataRow>
	{
		#region Fields
		private DataRow row;

		/// <summary>
		/// Delegate to be used for value conversion when nullable.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="name">column name</param>
		/// <param name="value">the value </param>
		/// <returns>converted value</returns>
		private delegate void Conversion<T>(string name, T value);
		#endregion

		#region Construction
		/// <summary>
		/// prevents the initialization of new instances of the <see cref="T:NullableDataRowWriter"/> class without parameters.
		/// </summary>
		private NullableDataRowWriter()
		{ }

		/// <summary>
		/// Initializes a new instance of the <see cref="T:NullableDataRowWriter"/> class with a given <see cref="T:DataRow"/>.
		/// </summary>
		/// <param name="dataRow">The data row.</param>
		public NullableDataRowWriter(DataRow dataRow)
		{
			if (dataRow == null)
				throw new ArgumentNullException("dataRow");

			this.row = dataRow;
		}
		#endregion

		#region INullableWriter Members
		/// <summary>
		/// The number of columns in the underlying row;
		/// </summary>
		/// <value></value>
		public int FieldCount
		{
			get { return row.Table.Columns.Count; }
		}

		/// <summary>
		/// Sets the underlying row.
		/// </summary>
		/// <value>The row.</value>
		public DataRow Row
		{
			get { return row; }
		}

		/// <summary>
		/// Gets the column name at the ordinal position.
		/// </summary>
		/// <param name="ordinal">The column ordinal.</param>
		/// <returns></returns>
		public string GetName(int ordinal)
		{
			if (ordinal < 0 || ordinal > (FieldCount - 1))
				throw new NullableException(String.Format(CultureInfo.CurrentCulture, InternalResources.OrdinalOutOfRangeException, ordinal.ToString(CultureInfo.CurrentCulture)));

			return row.Table.Columns[ordinal].ColumnName;
		}

		/// <summary>
		/// Gets the ordinal position of the column name.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns></returns>
		public int GetOrdinal(string name)
		{
			CheckIsField(name);
			return row.Table.Columns[name].Ordinal;
		}

		/// <summary>
		/// Sets the boolean value.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The boolean value to insert into the column.</param>
		public void SetBoolean(string name, bool value)
		{
			CheckIsField(name);
			SetNullable<bool>(name, value, SetNullableBoolean);
		}

		/// <summary>
		/// Sets the nullable boolean.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The nullable boolean value to insert into the column.</param>
		public void SetNullableBoolean(string name, bool value)
		{
			CheckIsField(name);
			// there is no way of telling a null boolean value
			SetRawValue(name, value);
		}

		/// <summary>
		/// Sets the byte.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The byte value to insert into the column.</param>
		public void SetByte(string name, byte value)
		{
			CheckIsField(name);
			SetNullable<byte>(name, value, SetNullableByte);
		}

		/// <summary>
		/// Sets the nullable byte.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The nullable byte value to insert into the column.</param>
		public void SetNullableByte(string name, byte value)
		{
			CheckIsField(name);
			if (value == Byte.MinValue)
				SetRawValue(name, null);
			else
				SetRawValue(name, value);
		}

		/// <summary>
		/// Sets the char.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The char value to insert into the column.</param>
		public void SetChar(string name, char value)
		{
			CheckIsField(name);
			SetNullable<char>(name, value, SetNullableChar);
		}

		/// <summary>
		/// Sets the nullable char.
		/// </summary>
		/// <param name="name">The name.</param>
		/// <param name="value">The value.</param>
		public void SetNullableChar(string name, char value)
		{
			CheckIsField(name);
			if (value == Char.MinValue)
				SetRawValue(name, null);
			else
				SetRawValue(name, value);
		}

		/// <summary>
		/// Sets the date time.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The datetime value to insert into the column.</param>
		public void SetDateTime(string name, DateTime value)
		{
			CheckIsField(name);
			SetNullable<DateTime>(name, value, SetNullableDateTime);
		}

		/// <summary>
		/// Sets the nullable date time.
		/// </summary>
		/// <param name="name">The name.</param>
		/// <param name="value">The value.</param>
		public void SetNullableDateTime(string name, DateTime value)
		{
			CheckIsField(name);
			if (value == DateTime.MinValue)
				SetRawValue(name, null);
			else
				SetRawValue(name, value);
		}

		/// <summary>
		/// Sets the decimal.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The decimal value to insert into the column.</param>
		public void SetDecimal(string name, decimal value)
		{
			CheckIsField(name);
			SetNullable<decimal>(name, value, SetNullableDecimal);
		}

		/// <summary>
		/// Sets the nullable decimal.
		/// </summary>
		/// <param name="name">The name.</param>
		/// <param name="value">The value.</param>
		public void SetNullableDecimal(string name, decimal value)
		{
			CheckIsField(name);
			if (value == 0)
				SetRawValue(name, null);
			else
				SetRawValue(name, value);
		}

		/// <summary>
		/// Sets the double.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The double value to insert into the column.</param>
		public void SetDouble(string name, double value)
		{
			CheckIsField(name);
			SetNullable<double>(name, value, SetNullableDouble);
		}

		/// <summary>
		/// Sets the nullable double.
		/// </summary>
		/// <param name="name">The name.</param>
		/// <param name="value">The value.</param>
		public void SetNullableDouble(string name, double value)
		{
			CheckIsField(name);
			if (value == 0)
				SetRawValue(name, null);
			else
				SetRawValue(name, value);
		}

		/// <summary>
		/// Sets the float.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The float value to insert into the column.</param>
		public void SetFloat(string name, float value)
		{
			CheckIsField(name);
			SetNullable<float>(name, value, SetNullableFloat);
		}

		/// <summary>
		/// Sets the nullable float.
		/// </summary>
		/// <param name="name">The name.</param>
		/// <param name="value">The value.</param>
		public void SetNullableFloat(string name, float value)
		{
			CheckIsField(name);
			if (value == 0)
				SetRawValue(name, null);
			else
				SetRawValue(name, value);
		}

		/// <summary>
		/// Sets the Guid.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The guid value to insert into the column.</param>
		public void SetGuid(string name, Guid value)
		{
			CheckIsField(name);
			SetNullable<Guid>(name, value, SetNullableGuid);
		}

		/// <summary>
		/// Sets the nullable GUID.
		/// </summary>
		/// <param name="name">The name.</param>
		/// <param name="value">The value.</param>
		public void SetNullableGuid(string name, Guid value)
		{
			CheckIsField(name);
			if (value == Guid.Empty)
				SetRawValue(name, null);
			else
				SetRawValue(name, value);
		}

		/// <summary>
		/// Sets the short integer.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The short value to insert into the column.</param>
		public void SetInt16(string name, short value)
		{
			CheckIsField(name);
			SetNullable<short>(name, value, SetNullableInt16);
		}

		/// <summary>
		/// Sets the nullable short integer.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The nullable short value to insert into the column.</param>
		public void SetNullableInt16(string name, short value)
		{
			CheckIsField(name);
			if (value == 0)
				SetRawValue(name, null);
			else
				SetRawValue(name, value);
		}

		/// <summary>
		/// Sets the integer.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The int value to insert into the column.</param>
		public void SetInt32(string name, int value)
		{
			CheckIsField(name);
			SetNullable<int>(name, value, SetNullableInt32);
		}

		/// <summary>
		/// Sets the nullable integer.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The nullable int value to insert into the column.</param>
		public void SetNullableInt32(string name, int value)
		{
			CheckIsField(name);
			if (value == 0)
				SetRawValue(name, null);
			else
				SetRawValue(name, value);
		}

		/// <summary>
		/// Sets the long integer.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The long value to insert into the column.</param>
		public void SetInt64(string name, long value)
		{
			CheckIsField(name);
			SetNullable<long>(name, value, SetNullableInt64);
		}

		/// <summary>
		/// Sets the nullable long integer.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The nullable long value to insert into the column.</param>
		public void SetNullableInt64(string name, long value)
		{
			CheckIsField(name);
			if (value == 0)
				SetRawValue(name, null);
			else
				SetRawValue(name, value);
		}

		/// <summary>
		/// Sets the string.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The string value to insert into the column.</param>
		public void SetString(string name, string value)
		{
			CheckIsField(name);
			// strings can take null values so no conversion required
			SetRawValue(name, value);
		}

		/// <summary>
		/// Sets the nullable string.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The nullable string value to insert into the column.</param>
		public void SetNullableString(string name, string value)
		{
			CheckIsField(name);
			if (String.IsNullOrEmpty(value))
				SetRawValue(name, null);
			else
				SetRawValue(name, value);
		}

		/// <summary>
		/// Sets the time span.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The timespan value to insert into the column.</param>
		public void SetTimeSpan(string name, TimeSpan value)
		{
			CheckIsField(name);
			SetNullable<TimeSpan>(name, value, SetNullableTimeSpan);
		}

		/// <summary>
		/// Sets the nullable time span.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The nullable timespan value to insert into the column.</param>
		public void SetNullableTimeSpan(string name, TimeSpan value)
		{
			CheckIsField(name);
			if (value == TimeSpan.MinValue)
				SetRawValue(name, null);
			else
				SetRawValue(name, value);
		}

		/// <summary>
		/// Sets a data value with conversion to the given type.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The object value to insert into the column.</param>
		/// <param name="type">The data type of the value to be inserted.</param>
		public void SetValue(string name, object value, Type type)
		{
			CheckIsField(name);
			switch (type.FullName)
			{
				case "System.Boolean":
					SetBoolean(name, (bool)value);
					break;
				case "System.Byte":
					SetByte(name, (byte)value);
					break;
				case "System.Char":
					SetChar(name, (char)value);
					break;
				case "System.DateTime":
					SetDateTime(name, (DateTime)value);
					break;
				case "System.Decimal":
					SetDecimal(name, (decimal)value);
					break;
				case "System.Double":
					SetDouble(name, (double)value);
					break;
				case "System.Single":
					SetFloat(name, (float)value);
					break;
				case "System.Guid":
					SetGuid(name, (Guid)value);
					break;
				case "System.Int16":
					SetInt16(name, (short)value);
					break;
				case "System.Int32":
					SetInt32(name, (int)value);
					break;
				case "System.Int64":
					SetInt64(name, (long)value);
					break;
				case "System.String":
					SetString(name, (string)value);
					break;
				case "System.TimeSpan":
					SetTimeSpan(name, (TimeSpan)value);
					break;
				default:
					SetRawValue(name, value);
					break;
			}
		}

		/// <summary>
		/// Sets a data value with conversion to the given type.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The nullable object value to insert into the column.</param>
		/// <param name="type">The data type of the value to be inserted.</param>
		public void SetNullableValue(string name, object value, Type type)
		{
			CheckIsField(name);
			switch (type.FullName)
			{
				case "System.Boolean":
					SetNullableBoolean(name, (bool)value);
					break;
				case "System.Byte":
					SetNullableByte(name, (byte)value);
					break;
				case "System.Char":
					SetNullableChar(name, (char)value);
					break;
				case "System.DateTime":
					SetNullableDateTime(name, (DateTime)value);
					break;
				case "System.Decimal":
					SetNullableDecimal(name, (decimal)value);
					break;
				case "System.Double":
					SetNullableDouble(name, (double)value);
					break;
				case "System.Single":
					SetNullableFloat(name, (float)value);
					break;
				case "System.Guid":
					SetNullableGuid(name, (Guid)value);
					break;
				case "System.Int16":
					SetNullableInt16(name, (short)value);
					break;
				case "System.Int32":
					SetNullableInt32(name, (int)value);
					break;
				case "System.Int64":
					SetNullableInt64(name, (long)value);
					break;
				case "System.String":
					SetNullableString(name, (string)value);
					break;
				case "System.TimeSpan":
					SetNullableTimeSpan(name, (TimeSpan)value);
					break;
				default:
					SetRawValue(name, value);
					break;
			}
		}

		/// <summary>
		/// Sets the type information for the column.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="type">The type.</param>
		public void SetFieldType(string name, Type type)
		{
			CheckIsField(name);
			row.Table.Columns[name].DataType = type;
		}

		/// <summary>
		/// Determines whether the specified name is valid field.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns>
		/// 	<c>true</c> if the specified name is a field; otherwise, <c>false</c>.
		/// </returns>
		public bool IsField(string name)
		{
			return row.Table.Columns.Contains(name);
		}

		/// <summary>
		/// Determines whether the named column can take a null value.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <returns>
		/// 	<c>true</c> if nullable; otherwise <c>false</c>.
		/// </returns>
		public bool IsNullable(string name)
		{
			CheckIsField(name);
			return row.Table.Columns[name].AllowDBNull;
		}

		#endregion

		#region Private Methods
		/// <summary>
		/// This generic method will be called by every interface method in the class.
		/// Using a generic method offers significantly less code and type-safety.
		/// Additionally, the methods can delegate data conversion to an appropriate
		/// method to be executed here.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="name">Column name</param>
		/// <param name="value">The value to be inserted into the column.</param>
		/// <param name="convert">Conversion delegate to invoke if the value is nullable</param>
		private void SetNullable<T>(string name, T value, Conversion<T> convert) where T : struct
		{
			if (IsNullable(name))
				convert(name, value);
			else
				SetRawValue(name, value);
		}

		/// <summary>
		/// Sets the raw data value checking for DBNull in the process
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <param name="value">The value to be inserted into the column.</param>
		private void SetRawValue(string name, object value)
		{
			if (value == null)
			{
				if (IsNullable(name))
					row[name] = DBNull.Value;
				else
					throw new NullableException(InternalResources.NullNotSupportedException);
			}
			else
				row[name] = value;
		}

		/// <summary>
		/// Checks if the field name is valid and throws exception if not.
		/// </summary>
		/// <param name="name">The column name.</param>
		/// <exception cref="IndexOutOfRangeException">The field name has not been found as a column of the <see cref="DataRow"/></exception>
		private void CheckIsField(string name)
		{
			if (!IsField(name))
				throw new NullableException(String.Format(CultureInfo.CurrentCulture, InternalResources.ColumnMissingException, name));
		}
		#endregion
	}
}
